Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit f55079c5e9af4817e64af69457775e7934b67b3e


Parents : ff4e448
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-26T06:35:03-05:00

feat: harden Docker configurations and commands

Changes
Diff

diff --git a/README.md b/README.md
index a900c8e2..6f15b874 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml
index 3e005f41..f23f4076 100644
--- a/docker-compose.coolify.yml
+++ b/docker-compose.coolify.yml
@@ -1,24 +1,35 @@
# Coolify Docker Compose deployment for MeshChatX.
# https://coolify.io/docs/knowledge-base/docker/compose
#
-# In Coolify, assign a domain with container port 8000, for example:
+# Assign a domain in Coolify with container port 8000, for example:
# https://meshchatx.example.com:8000
-# Coolify terminates TLS at the proxy. This stack serves plain HTTP inside the
-# container (MESHCHAT_NO_HTTPS=1) so Traefik can reach it without a self-signed
-# backend cert. Do not publish host ports unless you intend to bypass the proxy.
+# The :8000 suffix is the container port for the proxy, not the public URL port.
+#
+# Coolify terminates TLS at the proxy. Plain HTTP inside the container
+# (MESHCHAT_NO_HTTPS=1) avoids a self-signed HTTPS backend. Do not add host
+# ports unless you intend to bypass the proxy.
services:
- reticulum-meshchatx:
+ meshchatx:
image: ${MESHCHAT_IMAGE:-quad4io/meshchatx:latest}
restart: unless-stopped
+ init: true
+ user: "1000:1000"
security_opt:
- no-new-privileges:true
- expose:
- - "8000"
+ cap_drop:
+ - ALL
+ read_only: true
+ tmpfs:
+ - /tmp:noexec,nosuid,size=256m
+ - /home/meshchat:nosuid,size=64m
environment:
+ - SERVICE_URL_MESHCHATX_8000
+ - MESHCHAT_HOST=${MESHCHAT_HOST:-0.0.0.0}
+ - MESHCHAT_PORT=${MESHCHAT_PORT:-8000}
- MESHCHAT_NO_HTTPS=${MESHCHAT_NO_HTTPS:-1}
- MESHCHAT_AUTH=${MESHCHAT_AUTH:-1}
- - MESHCHAT_PORT=${MESHCHAT_PORT:-8000}
+ - MESHCHAT_HEADLESS=${MESHCHAT_HEADLESS:-1}
volumes:
- meshchatx-config:/config
healthcheck:
@@ -33,15 +44,10 @@ services:
timeout: 5s
retries: 3
start_period: 90s
- deploy:
- resources:
- limits:
- cpus: "2.0"
- memory: 1G
- pids: 512
- reservations:
- cpus: "0.25"
- memory: 256M
+ cpus: 2.0
+ mem_limit: 1g
+ mem_reservation: 256m
+ pids_limit: 512
volumes:
meshchatx-config:

diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 63f0f1c9..d24698f0 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -6,12 +6,32 @@ services:
container_name: reticulum-meshchatx
image: reticulum-meshchatx:local
restart: unless-stopped
+ init: true
+ user: "1000:1000"
security_opt:
- no-new-privileges:true
+ cap_drop:
+ - ALL
+ read_only: true
+ tmpfs:
+ - /tmp:noexec,nosuid,size=256m
+ - /home/meshchat:nosuid,size=64m
ports:
- 127.0.0.1:8000:8000
volumes:
- meshchatx-config:/config
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "python",
+ "-c",
+ "import ssl, urllib.request; urllib.request.urlopen('https://127.0.0.1:8000/api/v1/status', context=ssl._create_unverified_context())",
+ ]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 90s
deploy:
resources:
limits:

diff --git a/docker-compose.yml b/docker-compose.yml
index 52ca9a8a..81d8a337 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,12 +3,32 @@ services:
container_name: reticulum-meshchatx
image: ${MESHCHAT_IMAGE:-quad4io/meshchatx:latest}
restart: unless-stopped
+ init: true
+ user: "1000:1000"
security_opt:
- no-new-privileges:true
+ cap_drop:
+ - ALL
+ read_only: true
+ tmpfs:
+ - /tmp:noexec,nosuid,size=256m
+ - /home/meshchat:nosuid,size=64m
ports:
- 127.0.0.1:8000:8000
volumes:
- meshchatx-config:/config
+ healthcheck:
+ test:
+ [
+ "CMD",
+ "python",
+ "-c",
+ "import ssl, urllib.request; urllib.request.urlopen('https://127.0.0.1:8000/api/v1/status', context=ssl._create_unverified_context())",
+ ]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 90s
deploy:
resources:
limits:

diff --git a/docs/en/installation.md b/docs/en/installation.md
index db87df23..2cded43d 100644
--- a/docs/en/installation.md
+++ b/docs/en/installation.md
@@ -41,7 +41,13 @@ Manual run with a named volume for persistence:
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/lang/README.de.md b/lang/README.de.md
index 63f6affb..119ecb27 100644
--- a/lang/README.de.md
+++ b/lang/README.de.md
@@ -88,7 +88,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/lang/README.it.md b/lang/README.it.md
index feb3cd48..22bffc6b 100644
--- a/lang/README.it.md
+++ b/lang/README.it.md
@@ -88,7 +88,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/lang/README.ja.md b/lang/README.ja.md
index 7715eea8..55f41d57 100644
--- a/lang/README.ja.md
+++ b/lang/README.ja.md
@@ -88,7 +88,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/lang/README.ru.md b/lang/README.ru.md
index b21d5946..c1e40991 100644
--- a/lang/README.ru.md
+++ b/lang/README.ru.md
@@ -88,7 +88,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/lang/README.zh.md b/lang/README.zh.md
index 85850856..4fa15afe 100644
--- a/lang/README.zh.md
+++ b/lang/README.zh.md
@@ -88,7 +88,13 @@ docker compose up -d
```bash
docker run -d --name reticulum-meshchatx \
--restart unless-stopped \
+ --init \
+ --user 1000:1000 \
--security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \

diff --git a/meshchatx.rsm b/meshchatx.rsm
index 9d8d4296..28f488db 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/scripts/ci/docker-hardened-smoke.sh b/scripts/ci/docker-hardened-smoke.sh
new file mode 100755
index 00000000..2c8809e5
--- /dev/null
+++ b/scripts/ci/docker-hardened-smoke.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# Build the standard image and verify hardened docker compose stacks and docker run.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+cd "$ROOT"
+
+IMAGE="${MESHCHAT_DOCKER_SMOKE_IMAGE:-reticulum-meshchatx:local}"
+RUN_CONTAINER="${MESHCHAT_DOCKER_RUN_SMOKE_CONTAINER:-meshchatx-hardened-run-test}"
+RUN_PORT="${MESHCHAT_DOCKER_RUN_SMOKE_PORT:-18081}"
+TIMEOUT_SEC="${MESHCHAT_DOCKER_SMOKE_TIMEOUT:-180}"
+CONFIG_DIR="${MESHCHAT_DOCKER_SMOKE_CONFIG:-$(mktemp -d)}"
+
+cleanup() {
+ docker compose -f docker-compose.yml down >/dev/null 2>&1 || true
+ docker compose -f docker-compose.dev.yml down >/dev/null 2>&1 || true
+ docker compose -f docker-compose.coolify.yml down >/dev/null 2>&1 || true
+ docker rm -f "$RUN_CONTAINER" >/dev/null 2>&1 || true
+ if [ "${MESHCHAT_DOCKER_SMOKE_KEEP_CONFIG:-0}" != "1" ] && [ -n "${TMP_CONFIG_CREATED:-}" ]; then
+ rm -rf "$CONFIG_DIR"
+ fi
+}
+trap cleanup EXIT
+
+if [ -z "${MESHCHAT_DOCKER_SMOKE_CONFIG:-}" ]; then
+ TMP_CONFIG_CREATED=1
+fi
+chmod 777 "$CONFIG_DIR"
+
+wait_https() {
+ local port="$1"
+ local deadline=$((SECONDS + TIMEOUT_SEC))
+ while [ "$SECONDS" -lt "$deadline" ]; do
+ if out=$(curl -fsSk "https://127.0.0.1:${port}/api/v1/status" 2>/dev/null) &&
+ printf '%s' "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("status")=="ok" else 1)'; then
+ echo "$out"
+ return 0
+ fi
+ sleep 3
+ done
+ return 1
+}
+
+wait_container_healthy() {
+ local name="$1"
+ local deadline=$((SECONDS + TIMEOUT_SEC))
+ local state=""
+ while [ "$SECONDS" -lt "$deadline" ]; do
+ state=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null || echo missing)
+ if [ "$state" = "healthy" ]; then
+ return 0
+ fi
+ if [ "$state" = "unhealthy" ]; then
+ docker logs "$name" >&2 || true
+ return 1
+ fi
+ sleep 3
+ done
+ echo "Timed out waiting for container health (last state: ${state})" >&2
+ docker logs "$name" >&2 || true
+ return 1
+}
+
+echo "Building Docker image ${IMAGE}..."
+docker build -f Dockerfile -t "$IMAGE" .
+
+echo "=== docker-compose.yml (hardened) ==="
+MESHCHAT_IMAGE="$IMAGE" docker compose -f docker-compose.yml up -d --pull never
+wait_container_healthy reticulum-meshchatx
+wait_https 8000 >/dev/null
+docker compose -f docker-compose.yml down
+
+echo "=== docker-compose.dev.yml (hardened) ==="
+docker compose -f docker-compose.dev.yml up -d --pull never
+wait_container_healthy reticulum-meshchatx
+wait_https 8000 >/dev/null
+docker compose -f docker-compose.dev.yml down
+
+echo "=== docker-compose.coolify.yml (hardened) ==="
+MESHCHAT_IMAGE="$IMAGE" docker compose -f docker-compose.coolify.yml up -d --pull never
+coolify_cid=$(docker compose -f docker-compose.coolify.yml ps -q meshchatx)
+wait_container_healthy "$coolify_cid"
+docker exec "$coolify_cid" python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/api/v1/status').read().decode())" >/dev/null
+docker compose -f docker-compose.coolify.yml down
+
+echo "=== docker run (hardened) ==="
+docker run -d --name "$RUN_CONTAINER" \
+ --restart unless-stopped \
+ --init \
+ --user 1000:1000 \
+ --security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
+ --cpus=2.0 \
+ --memory=1g \
+ --memory-reservation=256m \
+ --pids-limit=512 \
+ -p "127.0.0.1:${RUN_PORT}:8000" \
+ -v "${CONFIG_DIR}:/config" \
+ "$IMAGE" >/dev/null
+
+status_json=$(wait_https "$RUN_PORT")
+echo "Docker hardened smoke passed: ${status_json}"

diff --git a/scripts/ci/docker-runtime-smoke.sh b/scripts/ci/docker-runtime-smoke.sh
index 0011b85a..2937c9f2 100755
--- a/scripts/ci/docker-runtime-smoke.sh
+++ b/scripts/ci/docker-runtime-smoke.sh
@@ -35,11 +35,18 @@ mkdir -p "$CONFIG_DIR"
echo "Starting container on host port ${HOST_PORT}..."
docker run -d \
--name "$CONTAINER" \
+ --init \
+ --user 1000:1000 \
+ --security-opt no-new-privileges:true \
+ --cap-drop ALL \
+ --read-only \
+ --tmpfs /tmp:noexec,nosuid,size=256m \
+ --tmpfs /home/meshchat:nosuid,size=64m \
--cpus=2.0 \
--memory=1g \
--memory-reservation=256m \
--pids-limit=512 \
- -p "${HOST_PORT}:8000" \
+ -p "127.0.0.1:${HOST_PORT}:8000" \
-v "${CONFIG_DIR}:/config" \
"$IMAGE" >/dev/null

diff --git a/tests/backend/test_docker_runtime_smoke.py b/tests/backend/test_docker_runtime_smoke.py
index 9b42851f..190cb122 100644
--- a/tests/backend/test_docker_runtime_smoke.py
+++ b/tests/backend/test_docker_runtime_smoke.py
@@ -13,6 +13,7 @@ import pytest
_REPO_ROOT = Path(__file__).resolve().parents[2]
_SMOKE_SCRIPT = _REPO_ROOT / "scripts" / "ci" / "docker-runtime-smoke.sh"
+_HARDENED_SMOKE_SCRIPT = _REPO_ROOT / "scripts" / "ci" / "docker-hardened-smoke.sh"
def _docker_available() -> bool:
@@ -56,6 +57,31 @@ def test_docker_image_serves_status():
assert '"status": "ok"' in result.stdout or '"status":"ok"' in result.stdout
+@pytest.mark.integration
+@pytest.mark.skipif(
+ os.environ.get("MESHCHAT_DOCKER_SMOKE") != "1",
+ reason="Set MESHCHAT_DOCKER_SMOKE=1 to run Docker hardened compose + run smoke",
+)
+@pytest.mark.skipif(not _docker_available(), reason="Docker is not available")
+def test_docker_hardened_compose_and_run_serve_status():
+ env = os.environ.copy()
+ env.setdefault("MESHCHAT_DOCKER_SMOKE_TIMEOUT", "240")
+ result = subprocess.run(
+ ["bash", str(_HARDENED_SMOKE_SCRIPT)],
+ cwd=_REPO_ROOT,
+ env=env,
+ capture_output=True,
+ text=True,
+ timeout=int(env["MESHCHAT_DOCKER_SMOKE_TIMEOUT"]) + 600,
+ check=False,
+ )
+ assert result.returncode == 0, (
+ f"Docker hardened smoke failed (exit {result.returncode})\n"
+ f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
+ )
+ assert '"status": "ok"' in result.stdout or '"status":"ok"' in result.stdout
+
+
@pytest.mark.integration
@pytest.mark.skipif(
os.environ.get("MESHCHAT_DOCKER_SMOKE") != "1",


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────